/* Copyright (c) 2006-2007 MetaCarta, Inc., published under a modified BSD license.
 * See http://svn.openlayers.org/trunk/openlayers/repository-license.txt
 * for the full text of the license. */

/**
 * @requires OpenLayers/Control/Panel.js
 * @requires OpenLayers/Control/Navigation.js
 * @requires OpenLayers/Control/DrawFeature.js
 * @requires OpenLayers/Handler/Point.js
 * @requires OpenLayers/Handler/Path.js
 * @requires OpenLayers/Handler/Polygon.js
 *
 * Class: OpenLayers.Control.Graticule
 */
OpenLayers.Control.Graticule = OpenLayers.Class( OpenLayers.Control, {
  /**
   * APIProperty: style
   * {Object} A hash of line style properties.
   */
  style: { 'strokeWidth': 1, 'strokeColor': '#000000' },

  /**
   * APIProperty: linesPerLatDeg
   * {Number} The number of lines to draw per degree in the latitudinal direction.
   */
  linesPerLatDeg: 2,

  /**
    * APIProperty: linesPerLonDeg
    * {Number} A hash of line style properties.
    */
  linesPerLonDeg: 2,

  /**
    * APIProperty: bottomGutterHeight
    * {Number} The height in pixels to leave for drawing the longitudinal labels.
    */
  bottomGutterHeight: 20,

  /**
    * APIProperty: rightGutterWidth
    * {Number} The width in pixels to leave for drawing the latitudinal labels.
    */
  rightGutterWidth: 40,

  minLabelSeparation: 80, // experimental for now..
  // guesswork, since we don't have access to fontmetrics functionality:
  maxLabelWidth: 40,
  maxLabelHeight: 15,



  /**
   * Constructor: OpenLayers.Control.Graticule
   *
   * Parameters:
   * options - {DOMElement} Options for control.
   */
  initialize: function(options) {
    OpenLayers.Control.prototype.initialize.apply(this, arguments);
  },

  /**
   * Method: destroy
   */
  destroy: function() {
    if (this.map) {
      this.map.events.unregister('moveend', this, this.redraw);
      this.map.events.unregister('mousemove', this, this.updateLabels);
    }
    OpenLayers.Control.prototype.destroy.apply(this, arguments);
  },

  // a list of all divs used to contain/position the longitudinal
  // labels (we re-use and reposition them so we don't recreate them
  // all the time):
  lonLabelDivs: [],
  // a list of all divs used to contain/position the longitudinal
  // labels (we re-use and reposition them so we don't recreate them
  // all the time):
  latLabelDivs: [],

  /**
   * Method: draw
   * calls the default draw, and then activates mouse defaults.
   */
  draw: function() {
    OpenLayers.Control.prototype.draw.apply(this, arguments);
    this.map.events.register('moveend', this, this.addGraticules);
    // I don't know why, but the 'drag' family of updates don't seem
    // to fire, so we attach to move events and check to see if it is
    // a drag:
    this.map.events.register('mousemove', this, this.updateLabels);

    // utility to make things a bit clearer below:
    var createDiv = function (className, style) {
      var div = document.createElement('div');
      div.className = this.displayClass + className;
      for (var attr in style) {
        div.style[attr] = style[attr];
      }
      return div;
    };

    // add a div for the longitudinal labels, and attach to the
    // viewport container (viewPortDiv isn't publicly documented, so
    // I'm not sure how safe this is -- however, parentNode isn't set
    // on this particular div, so we can't rely on that.  We also
    // can't just make the control div cover the entire area, because
    // then it blocks mousewheel zoom!):
    if (this.bottomGutterHeight && this.bottomGutterHeight > 0) {
      this.lonLabDivContainer = createDiv('lonlabcontainer',
                                          {display: 'block',
                                           position: 'absolute',
                                           bottom: 0,
                                           visibility: 'visible',
                                           width: '100%',
                                           height: this.bottomGutterHeight});
      this.map.viewPortDiv.appendChild(this.lonLabDivContainer);
    }
    // ditto for the right gutter (latitudinal labels):
    if (this.rightGutterWidth && this.rightGutterWidth > 0) {
      this.latLabDivContainer = createDiv('latlabcontainer',
                                          {display: 'block',
                                           position: 'absolute',
                                           right: 0,
                                           visibility: 'visible',
                                           height: '100%',
                                           width: this.rightGutterWidth});
      this.map.viewPortDiv.appendChild(this.latLabDivContainer);
    }

    // create longitudinal labels (10 of them for now):
    for (var i = 0; i<10; i++) {
      var llDiv = createDiv('lonlab',
                            {display: 'block',
                             position: 'absolute',
                             bottom: 0,
                             fontSize: 'xx-small',
                             visibility: 'visible'});
      this.lonLabDivContainer.appendChild(llDiv);
      this.lonLabelDivs[this.lonLabelDivs.length] = llDiv;
    }
    // create latitudinal labels (10 of them for now):
    for (var i = 0; i<10; i++) {
      var llDiv = createDiv('latlab',
                            {display: 'block',
                             position: 'absolute',
                             left: 7,
                             fontSize: 'xx-small',
                             visibility: 'visible'});
      this.latLabDivContainer.appendChild(llDiv);
      this.latLabelDivs[this.latLabelDivs.length] = llDiv;
    }
    this.addGraticules();
    return this.div;
  },

  setMap: function(map) {
    OpenLayers.Control.prototype.setMap.apply(this, arguments);
    this.vectorLayer = new OpenLayers.Layer.Vector("Graticules",
                                                   { displayInLayerSwitcher: false });
    map.addLayer(this.vectorLayer);
    map.raiseLayer(this.vectorLayer, -6);
  },

  /* Draw graticules
   *
   *@param this pointer to this object.
   */
  addGraticules: function(){
    //Delete old graticule
    if (!this.vectorLayer) {
      return;
    }
    this.vectorLayer.destroyFeatures();
    var extent = this.map.getExtent();
    if (!extent) {
      return;
    }

    // set z-index on the labels containers (it doesn't get set until
    // after draw() has finished, so we can't do it there):
    if (!this.lonLabDivContainer.style.zIndex) {
      this.lonLabDivContainer.style.zIndex = this.div.style.zIndex;
    }
    if (!this.latLabDivContainer.style.zIndex) {
      this.latLabDivContainer.style.zIndex = this.div.style.zIndex;
    }

    // calculate drawing units (extents are based on lonlat degrees,
    // this will be used in conversions):
    var res = this.map.getResolution();

    // these are the extents of the visible graticule lines, ie not
    // including the gutters:
    var el = extent.left;
    // some space on the right; if we draw labels on the left, it will
    // interfere with the zoom widget:
    var er = extent.right - this.rightGutterWidth*res;
    var et = extent.top;
    // space on the bottom to draw the degree labels:
    var eb = extent.bottom + this.bottomGutterHeight*res;

    // sizes that we will need to draw the lines and labels:
    var widthInDegrees = this.map.getExtent().getWidth() - this.rightGutterWidth*res;
    // Note that we prune off the right gutter, to avoid boundary clashes:
    var widthInPixels =  this.map.getSize().w-this.rightGutterWidth;
    var lonDegreesStep = this.calculateDegreeSep(widthInDegrees, widthInPixels);

    var heightInDegrees = this.map.getExtent().getHeight() - this.bottomGutterHeight*res;
    var heightInPixels =  this.map.getSize().h - this.bottomGutterHeight;
    var latDegreesStep = this.calculateDegreeSep(heightInDegrees, heightInPixels);

    // calculate density of lines -- currently this is hard-coded; a
    // later implementation might calculate the line density as a
    // function of map size in pixels or something like that.  For now
    // we just use an empirically-determined value to decide whether
    // to draw lines for fractional degrees or not:
    var drawSubLines = res < 1.0/24.0;

    // fractional values of a degree that each line is drawn (if we
    // intend to go that fine):
    var latInc = drawSubLines ? 1/this.linesPerLatDeg : 1;
    var lonInc = drawSubLines ? 1/this.linesPerLonDeg : 1;

    // minimum starting latitude and longitudinal lines drawn:
    var latStart = Math.ceil(eb / latInc) * latInc;
    var lonStart = Math.ceil(el / lonInc) * lonInc;

    // now draw the actual grid lines by creating line features on the
    // vector layer:
    var grid = [];
    for (var lat = latStart; lat < et; lat += latInc ) {
      var latLinePoints = [];
      latLinePoints.push(new OpenLayers.Geometry.Point(el, lat));
      // slightly longer line on label steps:
      if (this.rightGutterWidth && lat%latDegreesStep == 0) {
        latLinePoints.push(new OpenLayers.Geometry.Point(er+10*res, lat));
      }
      else {
        latLinePoints.push(new OpenLayers.Geometry.Point(er, lat));
      }
      grid.push(new OpenLayers.Feature.Vector(
                  new OpenLayers.Geometry.LineString(latLinePoints),
                  null, this.style));
    }
    for (var lon = lonStart; lon < er; lon += lonInc) {
      var lonLinePoints = [];
      lonLinePoints.push(new OpenLayers.Geometry.Point(lon, et));
      // slightly longer line on label steps:
      if (this.bottomGutterHeight && lon%lonDegreesStep == 0) {
        lonLinePoints.push(new OpenLayers.Geometry.Point(lon, eb-7*res));
      }
      else {
        lonLinePoints.push(new OpenLayers.Geometry.Point(lon, eb));
      }

      grid.push(new OpenLayers.Feature.Vector(
                  new OpenLayers.Geometry.LineString(lonLinePoints),
                  null, this.style));
    }
    this.vectorLayer.addFeatures(grid);

    // Drawing longitudinal labels.
    // Round boundaries to degree steps:
    var startLonLabVal = Math.ceil(el/lonDegreesStep) * lonDegreesStep;
    // Used to index the following *four* loops:
    var i = 0;
    for (i = 0, deg = startLonLabVal;
         i < this.lonLabelDivs.length && deg < er-this.rightGutterWidth*res;
         i++, deg+=lonDegreesStep) {
      this.lonLabelDivs[i].innerHTML = OpenLayers.Number.format(deg, 1);
      this.lonLabelDivs[i].style.left = (deg-el)/res;
    }
    // Remove any unused divs from the display (note that i is not
    // reset from the previous loop, we iterate to the end):
    for ( ; i < this.lonLabelDivs.length; i++) {
      this.lonLabelDivs[i].innerHTML = '';
    }

    // Drawing latitudinal labels.
    // Round boundaries to degree steps:
    var startLatLabVal = Math.ceil(eb / latDegreesStep) * latDegreesStep;
    for (i = 0, deg = startLatLabVal;
         i < this.latLabelDivs.length && deg +this.bottomGutterHeight*res < et;
         i++, deg+=latDegreesStep) {
      this.latLabelDivs[i].innerHTML = OpenLayers.Number.format(deg, 1);
      this.latLabelDivs[i].style.bottom = (deg-eb)/res + this.bottomGutterHeight;
    }
    // Remove any unused divs from the display (note that i is not
    // reset from the previous loop, we iterate to the end):
    for ( ; i < this.latLabelDivs.length; i++) {
      this.latLabelDivs[i].innerHTML = '';
    }

  },

  updateLabels: function(evt) {
    // Since this is invoked for every mousemove, not just drags, we
    // check if we are actually in the middle of a drag:
    if (this.map.dragging) {
      if (this.lastXy) {
        var dx = evt.xy.x - this.lastXy.x;
        var dy = evt.xy.y - this.lastXy.y;

        // Although style.{left,bottom} is initialised as a number, it
        // is converted to a string, so we need to convert back from
        // '23.14px' to 23.14 in order to do the arithmetic updates:
        for (var i = 0; i < this.lonLabelDivs.length; i++) {
          this.lonLabelDivs[i].style.left = parseFloat(this.lonLabelDivs[i].style.left) + dx;
        }
        for (i = 0; i < this.latLabelDivs.length; i++) {
          this.latLabelDivs[i].style.bottom = parseFloat(this.latLabelDivs[i].style.bottom) - dy;
        }
      }
      this.lastXy = evt.xy;
    }
  },

  calculateDegreeSep: function(range, length) {
    // All of this is ripped from the algorithms by(?) Jason Waring in DMPL.

    // local utilities:
    var log10 = function(arg) {
      return Math.log(arg)/Math.log(10);
    };
    var stepUp = function(v) {
      if (v === 0.0) return 0.0;

      var absV = Math.abs(v);
      var exp = Math.pow(10.0, Math.floor(log10(absV)));
      var mant = absV / exp;

      if (parseInt(mant, 10) == 1) {
        mant = 2.0;
      } else if (parseInt(mant, 10) === 2) {
        mant = 5.0;
      } else if (parseInt(mant, 10) === 5) {
        mant = 10.0;
      } else if (parseInt(mant, 10) === 10) {
        mant = 20.0;
      }
      return (v > 0.0) ? (mant * exp) : (-mant * exp);
    };
    var stepDown = function(v) {
      if (v === 0.0) return 0.0;

      var absV = Math.abs(v);
      var exp = Math.pow(10.0, Math.floor(log10(absV)));
      var mant = absV / exp;

      if (parseInt(mant, 10) === 1) {
        mant = 0.5;
      } else if (parseInt(mant, 10) === 2) {
        mant = 1.0;
      } else if (parseInt(mant, 10) === 5) {
        mant = 2.0;
      } else if (parseInt(mant, 10) === 10) {
        mant = 5.0;
      }
      return (v > 0.0) ? (mant * exp) : (-mant * exp);
    };
    var getStep = function(v) {
      if (v === 0.0) return 0.0;

      var absV = Math.abs(v);
      var exp = Math.pow(10.0, Math.floor(log10(absV)));
      var mant = absV / exp;

      if (mant < 1.4) {
        mant = 1.0;
      } else if (mant < 3.2) {
        mant = 2.0;
      } else if (mant < 7.1) {
        mant = 5.0;
      } else {
        mant = 10.0;
      }
      return (v > 0.0) ? (mant * exp) : (-mant * exp);
    };

    // (this will be different for lat vs lon labels, but it'll do for now):
    var minSep = this.maxLabelHeight + 2*this.minLabelSeparation;

    // desired intervals:
    var numIntervals = parseInt(length/minSep, 10);

    var step = 0.0;
    if (numIntervals > 0) {
      step = getStep(range/numIntervals);
      while ((length / (range / step)) < minSep) {
        step = stepUp(step);
      }
      while ((range / step) < 2.0) {
        step = stepDown(step);
      }
    }
    else {
      step = getStep(range);
    }

    return step;
  },

  CLASS_NAME: "OpenLayers.Control.Graticule"
});
